package pa3;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;

import pa3.SystemManager;

public class HostImpl extends UnicastRemoteObject implements Host, Runnable {

	/**
	 * 
	 */
	private static final long serialVersionUID = -14019794110865162L;
	
	private int failureProbability = 0;
	private String name;
	private HashMap<String, Host> otherHosts;
	private HashMap<String, Timer> checkTimers;
	//private boolean isCrashed = false;
	private Timer checkScheduleTimer;
	
	protected HostImpl(String name, int failureProbability) throws RemoteException {
		super();
		this.name = name;
		this.failureProbability = failureProbability;
		this.checkTimers = new HashMap<String, Timer>();
	}

	/**
	 * Gets called when the host gets checked. If the checked Host is still alive it
	 * will respond by calling the caller's checkResponse method.
	 */
	@Override
	public void checkHost(Host caller, String callerName) throws RemoteException {
		//generate a random number between 0 and 100 which 
		//determines if the host will respond to the message
		//or if he will crash
		Random generator = new Random();
		
		int random = generator.nextInt(100);
		
		if(random > this.failureProbability){
			try{
				//send the response message
				caller.checkResponse(this.name);
			}catch(RemoteException e){
				//if an exception occurs here, the caller crashed in the meantime
				failureDetected(callerName);
			}
		}
		else{
			crash();
		}
	}
	
	/**
	 * This method simulates a crash 
	 */
	private void crash() {
		logEvent("crashed");
		otherHosts = null;
		if(checkTimers != null){
			cancelCheckTimers();
			checkTimers = null;
		}		
		if(checkScheduleTimer != null){
			checkScheduleTimer.cancel();
			checkScheduleTimer = null;
		}
		
		//go to sleep for 15s
		try {
			Thread.sleep(15000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		//"reboot" and join the group again
		//isCrashed = false;
		joinGroup();
	}

	private void cancelCheckTimers() {
		if(checkTimers != null){
			Iterator<String> iter = checkTimers.keySet().iterator();
			while(iter.hasNext()){
				String hostName = iter.next();
				checkTimers.get(hostName).cancel();
			}

		}
		
	}

	/** 
	 * This method connects the host to the other hosts
	 */
	private synchronized void joinGroup() {
		try {
			Registry registry = LocateRegistry.getRegistry("localhost", Registry.REGISTRY_PORT);
			SystemManager systemManager = (SystemManager) registry.lookup(SystemManager.SERVICENAME);            
			this.otherHosts = systemManager.register(this, this.name);
			this.otherHosts.remove(this.name);
			
			//tell the other hosts that it's here now
			Iterator<String> iter = otherHosts.keySet().iterator();
			while(iter.hasNext()){
				String otherHostName = iter.next();
				Host otherHost = otherHosts.get(otherHostName);
				try {
					otherHost.addOtherHost(this, this.name);				
				} catch (RemoteException e) {
					//if an exception happens here, one of the other hosts is unavailable as well
					failureDetected(otherHostName);				
				}
			}
			
			logEvent("joined the group");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * Checks for a Signal from an other Host. If he doesn't reply within CHECK_WAIT_TIME
	 * the other host is considered dead and gets reported. 
	 * @param host
	 */
	private void checkOtherHost(String hostName){
		if(otherHosts != null){
			logEvent("checking " + hostName);
			synchronized(otherHosts){
				//get the host from the map containing the other hosts
				Host host = otherHosts.get(hostName);
				
				//start timer 
				Timer t = new Timer();
				t.schedule(new DetectedFailureTask(this, hostName), Host.CHECK_WAIT_TIME);
				
				synchronized(checkTimers){
					//add the timer to the map containing the active timers
					checkTimers.put(hostName, t);
				}
				
				
				//send the checkHost request to the other host
				try {
					if(host != null)
						host.checkHost(this, name);
					//if he gets a response in time, the timer t will be cancelled
				
				} catch (RemoteException e) {
					//if an exception happens here, the host was dead already
					failureDetected(hostName);
				}
			}	
		}				
	}
	
	/**
	 * Cancels the timer associated with the given hostname
	 * @param hostname
	 */
	private synchronized void cancelTimerForHost(String hostName){
		if(checkTimers != null){
			Timer t = checkTimers.get(hostName);
			if(t != null){
				t.cancel();
				checkTimers.remove(hostName);
			}
		}
		
	}
	
	class DetectedFailureTask extends TimerTask{

		private String detectedHostName;
		private HostImpl context;
		
		public DetectedFailureTask(HostImpl context, String hostName){
			super();
			this.detectedHostName = hostName;
			this.context = context;
		}
		public void run() {
			logEvent(detectedHostName + " failed to respnd");
			context.failureDetected(detectedHostName);
		}
		
	}

	@Override
	public String getName() throws RemoteException {
		return this.name;
	}
	
	class CheckForHosts extends TimerTask{
		private HostImpl context;
		
		public CheckForHosts(HostImpl context){
			this.context = context;
		}
		@Override
		public void run() {
			context.checkOtherHosts();		
		}
		
	}
	
	public void checkOtherHosts(){
		synchronized(otherHosts){
			if(otherHosts != null){
				HashMap<String, Host> otherHostsCpy = (HashMap<String, Host>) otherHosts.clone();
				Iterator<String> iter = otherHostsCpy.keySet().iterator();
				while(iter.hasNext()){
					checkOtherHost(iter.next());
				}
				rescheduleChecks();
			}
		}	
	}
	
	private void rescheduleChecks(){
		if(this.checkScheduleTimer == null){
			this.checkScheduleTimer = new Timer();
		}
		this.checkScheduleTimer.schedule(new CheckForHosts(this), Host.CHECK_SCHEDULE_INTERVAL);
	}
	
	/**
	 * Gets called when a failure of a host has been detected. The method informs all other hosts
	 * about the failure by removing the dead host from their lists
	 * @param detectedHostName
	 */
	public void informOtherHosts(String detectedHostName) {
		synchronized(otherHosts){
			HashMap<String, Host> otherHostsCpy = (HashMap<String, Host>) otherHosts.clone();
			Iterator<String> iter = otherHostsCpy.keySet().iterator();
			while(iter.hasNext()){
				String otherHostName = iter.next();
				Host otherHost = otherHosts.get(otherHostName);
				try {
					otherHost.removeOtherHost(detectedHostName);				
				} catch (RemoteException e) {
					//if an exception happens here, one of the other hosts is unavailable as well
					failureDetected(otherHostName);				
				}
			}
		}				
	}
	
	/** gets called whenever a dead Host has been detected
	 * 
	 * @param hostName
	 */
	public void failureDetected(String detectedHostName){
		logEvent("detected failure of Host " + detectedHostName);
		
		//remove detected Host from the map
		try {
			removeOtherHost(detectedHostName);
		} catch (RemoteException e1) {
			e1.printStackTrace();
		}
		//inform the other hosts about the failure
		informOtherHosts(detectedHostName);
	}
	
	private void logEvent(String eventMsg){
		System.out.println("[" + new Date(System.currentTimeMillis()).toString() + "]:"
				+ "Host " + this.name + ": " + eventMsg);
	}

	/**
	 * Gets called when a new Host has joined the group.
	 */
	@Override
	public synchronized void addOtherHost(Host otherHost, String otherHostName) throws RemoteException{
		if(this.otherHosts != null)
			this.otherHosts.put(otherHostName, otherHost);
	}

	@Override
	public synchronized void removeOtherHost(String detectedHostName) throws RemoteException {
		if(otherHosts != null)
			otherHosts.remove(detectedHostName);
		cancelTimerForHost(detectedHostName);
	}

	/**
	 * 
	 */
	@Override
	public void run() {
		joinGroup();
		rescheduleChecks();
	}

	@Override
	public synchronized void checkResponse(String hostName) throws RemoteException {
		logEvent("received response signal from " + hostName);
		//cancel the timer for the host
		cancelTimerForHost(hostName);		
	}

}
